package ws.quokka.maven.repo_plugin;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.metadata.ArtifactMetadataSource;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactCollector;
import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.artifact.resolver.filter.ScopeArtifactFilter;
import org.apache.maven.artifact.versioning.ArtifactVersion;
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
import org.apache.maven.artifact.versioning.Restriction;
import org.apache.maven.artifact.versioning.VersionRange;
import org.apache.maven.execution.RuntimeInformation;
import org.apache.maven.model.License;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectBuilder;
import org.apache.maven.project.ProjectBuildingException;
import org.apache.maven.project.artifact.InvalidDependencyVersionException;
import org.apache.maven.shared.dependency.tree.DependencyNode;
import org.apache.maven.shared.dependency.tree.DependencyTreeBuilder;
import org.apache.maven.shared.dependency.tree.traversal.DependencyNodeVisitor;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

/**
 * Displays the dependency tree for this project.
 *
 * @author <a href="mailto:markhobson@gmail.com">Mark Hobson</a>
 * @author Andrew O'Malley
 * @version $Id: TreeMojo.java 614346 2008-01-22 22:05:09Z brianf $
 * @goal quokkatree
 * @requiresDependencyResolution test
 * @since 2.0-alpha-5
 *        <p/>
 *        Note: Modified by Andrew O'Malley
 *        This is a hacked version of the maven dependency plugin that exports information about maven
 *        dependencies so that they may be imported into quokka
 */
public class QuokkaTreeMojo extends AbstractMojo {
    // fields -----------------------------------------------------------------

    /**
     * The Maven project.
     *
     * @parameter expression="${project}"
     * @required
     * @readonly
     */
    private MavenProject project;

    /**
     * The artifact repository to use.
     *
     * @parameter expression="${localRepository}"
     * @required
     * @readonly
     */
    private ArtifactRepository localRepository;

    /**
     * The artifact factory to use.
     *
     * @component
     * @required
     * @readonly
     */
    private ArtifactFactory artifactFactory;

    /**
     * The artifact metadata source to use.
     *
     * @component
     * @required
     * @readonly
     */
    private ArtifactMetadataSource artifactMetadataSource;

    /**
     * The artifact collector to use.
     *
     * @component
     * @required
     * @readonly
     */
    private ArtifactCollector artifactCollector;

    /**
     * The dependency tree builder to use.
     *
     * @component
     * @required
     * @readonly
     */
    private DependencyTreeBuilder dependencyTreeBuilder;

    /**
     * If specified, this parameter will cause the dependency tree to be written to the path specified, instead of
     * writing to the console.
     *
     * @parameter expression="${outputFile}"
     * @since 2.0-alpha-5
     */
    private File outputFile;

    /**
     * The scope to filter by when resolving the dependency tree, or <code>null</code> to include dependencies from
     * all scopes. Note that this feature does not currently work due to MNG-3236.
     *
     * @parameter expression="${scope}"
     * @see <a href="http://jira.codehaus.org/browse/MNG-3236">MNG-3236</a>
     * @since 2.0-alpha-5
     */
    private String scope;

    /**
     * Whether to include omitted nodes in the serialized dependency tree.
     *
     * @parameter expression="${verbose}" default-value="false"
     * @since 2.0-alpha-6
     */
    private boolean verbose;

    /**
     * The token set name to use when outputting the dependency tree. Possible values are <code>whitespace</code>,
     * <code>standard</code> or <code>extended</code>, which use whitespace, standard or extended ASCII sets
     * respectively.
     *
     * @parameter expression="${tokens}" default-value="standard"
     * @since 2.0-alpha-6
     */
    private String tokens;

    /**
     * A comma-separated list of artifacts to filter the serialized dependency tree by, or <code>null</code> not to
     * filter the dependency tree. The artifact syntax is defined by <code>StrictPatternIncludesArtifactFilter</code>.
     *
     * @parameter expression="${includes}"
     * @see org.apache.maven.shared.artifact.filter.StrictPatternIncludesArtifactFilter
     * @since 2.0-alpha-6
     */
    private String includes;

    /**
     * A comma-separated list of artifacts to filter from the serialized dependency tree, or <code>null</code> not to
     * filter any artifacts from the dependency tree. The artifact syntax is defined by
     * <code>StrictPatternExcludesArtifactFilter</code>.
     *
     * @parameter expression="${excludes}"
     * @see org.apache.maven.shared.artifact.filter.StrictPatternExcludesArtifactFilter
     * @since 2.0-alpha-6
     */
    private String excludes;

    /**
     * Runtime Information used to check the Maven version
     *
     * @component role="org.apache.maven.execution.RuntimeInformation"
     * @since 2.0
     */
    private RuntimeInformation rti;

    /**
     * The computed dependency tree root node of the Maven project.
     */
    private DependencyNode rootNode;


    /**
     * Used to look up Artifacts in the remote repository.
     *
     * @parameter expression="${component.org.apache.maven.artifact.resolver.ArtifactResolver}"
     * @required
     * @readonly
     */
    protected org.apache.maven.artifact.resolver.ArtifactResolver resolver;

    /**
     * Artifact factory, needed to download source jars.
     *
     * @component role="org.apache.maven.project.MavenProjectBuilder"
     * @required
     * @readonly
     */
    protected MavenProjectBuilder mavenProjectBuilder;

    /**
     * List of Remote Repositories used by the resolver
     *
     * @parameter expression="${project.remoteArtifactRepositories}"
     * @readonly
     * @required
     */
    protected java.util.List remoteRepos;

    /**
     * List of Remote Repositories used by the resolver
     *
     * @parameter expression="${artifact}"
     * @required
     */
    protected String artifactId;

    // Mojo methods -----------------------------------------------------------

    protected Set resolveDependencyArtifacts(MavenProject theProject)
            throws ArtifactResolutionException, ArtifactNotFoundException, InvalidDependencyVersionException {
        Set artifacts = theProject.createArtifacts(artifactFactory, Artifact.SCOPE_RUNTIME,
                new ScopeArtifactFilter(Artifact.SCOPE_RUNTIME));

        for (Iterator i = artifacts.iterator(); i.hasNext();) {
            Artifact artifact = (Artifact) i.next();
            // resolve the new artifact
            this.resolver.resolve(artifact, remoteRepos, localRepository);
        }
        return artifacts;
    }

    /*
     * @see org.apache.maven.plugin.Mojo#execute()
     */
    public void execute() throws MojoExecutionException, MojoFailureException {
        try {
            assertVersion();

            // Generate DOM output
            Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
            Element rootEl = (Element) document.appendChild(document.createElement("artifact"));

            // First get the pom of the artifact itself to discover the optional runtime dependencies
            Element dependenciesEl = (Element) rootEl.appendChild(document.createElement("dependencies"));
            MavenProject pom = getArtifactPom();

            // Get the description
            if (pom.getDescription() != null) {
                Element descriptionEl = document.createElement("description");
                rootEl.appendChild(descriptionEl);
                descriptionEl.appendChild(document.createTextNode(pom.getDescription()));
            }

            if (pom.getLicenses().size() != 0) {
                Element licensesEl = document.createElement("licenses");
                rootEl.appendChild(licensesEl);
                for (Iterator i = pom.getLicenses().iterator(); i.hasNext();) {
                    License license = (License) i.next();
                    Element licenseEl = document.createElement("license");
                    licenseEl.setAttribute("name", license.getName());
                    licenseEl.setAttribute("url", license.getUrl());
                    licensesEl.appendChild(licenseEl);
                }
            }

            for (Iterator i = resolveDependencyArtifacts(pom).iterator(); i.hasNext();) {
                Artifact dependency = (Artifact) i.next();
                artifactAttributes(dependency, (Element) dependenciesEl.appendChild(document.createElement("dependency")));
            }

            // Then resolve the runtime path to work out exactly which artifacts get resolved
            // (takes into account exclusions and conflicts)
//            pom.setDependencyArtifacts(pom.createArtifacts(artifactFactory, Artifact.SCOPE_RUNTIME, null));
//            project.setArtifacts(Collections.singleton(getArtifact()));
            rootNode = dependencyTreeBuilder.buildDependencyTree(project, localRepository, artifactFactory,
                    artifactMetadataSource, null, artifactCollector);

            // Convert the path to xml
            Element pathEl = (Element) rootEl.appendChild(document.createElement("path"));
            DependencyNodeVisitor visitor = new MyDependencyNodeVisitor(pathEl);
            rootNode.accept(visitor);

            String xml = serialiseXml(document);

            if (outputFile != null) {
                DependencyUtil.write(xml, outputFile, getLog());

                getLog().info("Wrote dependency tree to: " + outputFile);
            } else {
                DependencyUtil.log(xml, getLog());
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private MavenProject getArtifactPom() throws ProjectBuildingException {
        Artifact artifact = getArtifact();
        Artifact pomArtifact = artifactFactory.createArtifact(artifact.getGroupId(), artifact.getArtifactId(), artifact
                .getVersion(), Artifact.SCOPE_RUNTIME, "pom");

        remoteRepos = remoteRepos != null ? remoteRepos : Collections.EMPTY_LIST; // Work out how to fix stub to remove this
        MavenProject pomProject = mavenProjectBuilder.buildFromRepository(pomArtifact, remoteRepos, localRepository);
        return pomProject;
    }

    private Artifact getArtifact() {
        // Manually split as StringUtils swallows tokens
        String[] tokens = split(artifactId);

        if (tokens.length != 4 && tokens.length != 5) {
            throw new RuntimeException("artifactId format is groupId:artifactId:type:classifier:version. value=" + artifactId);
        }
        Artifact artifact = artifactFactory.createArtifact(tokens[0], tokens[1], tokens[4], Artifact.SCOPE_RUNTIME, tokens[2]);
        return artifact;
    }

    private String[] split(String string) {
        List tokens = new ArrayList();
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < string.length(); i++) {
            char ch = string.charAt(i);
            if (ch == ':') {
                String token = sb.toString().trim();
                tokens.add(token.length() == 0 ? null : token);
                sb = new StringBuffer();
            } else {
                sb.append(ch);
            }
        }
        String token = sb.toString().trim();
        tokens.add(token.length() == 0 ? null : token);
        return (String[]) tokens.toArray(new String[tokens.size()]);
    }

    private void assertVersion() throws MojoExecutionException {
        ArtifactVersion detectedMavenVersion = rti.getApplicationVersion();
        VersionRange vr;
        try {
            vr = VersionRange.createFromVersionSpec("[2.0.8,)");
            if (!containsVersion(vr, detectedMavenVersion)) {
                getLog().warn(
                        "The tree mojo requires at least Maven 2.0.8 to function properly. You may get eroneous results on earlier versions");
            }
        }
        catch (InvalidVersionSpecificationException e) {
            throw new MojoExecutionException(e.getLocalizedMessage());
        }
    }

    // public methods ---------------------------------------------------------

    /**
     * Gets the Maven project used by this mojo.
     *
     * @return the Maven project
     */
    public MavenProject getProject() {
        return project;
    }

    /**
     * Gets the computed dependency tree root node for the Maven project.
     *
     * @return the dependency tree root node
     */
    public DependencyNode getDependencyTree() {
        return rootNode;
    }

    // private methods --------------------------------------------------------

    private String serialiseXml(Document document) throws TransformerException {
        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        try {
            // Required for jdk1.5 as it ignores indent-amount below
            transformerFactory.setAttribute("indent-number", new Integer(4));
        } catch (Exception e) {
            // Ignore ... dies on jdk1.4, but indent-amount below works
        }

        Transformer transformer = transformerFactory.newTransformer();

        transformer.setOutputProperty(OutputKeys.STANDALONE, "no"); // Doesn't seem to be a way to kill this ...
        transformer.setOutputProperty(OutputKeys.METHOD, "xml");
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
        StringWriter writer = new StringWriter();
        transformer.transform(new DOMSource(document), new StreamResult(writer));

        return writer.toString();
    }

    public class MyDependencyNodeVisitor implements DependencyNodeVisitor {
        Element current;
        boolean first = true;

        public MyDependencyNodeVisitor(Element rootElement) {
            this.current = rootElement;
        }

        public boolean visit(DependencyNode node) {
            if (first) {
                first = false;
                return true; // Ingore root
            }

            current = (Element) current.appendChild(current.getOwnerDocument().createElement("dependency"));

            // Artifact info
            Artifact artifact = node.getArtifact();
            artifactAttributes(artifact, current);

            // Dependency node info
            attribute("originalScope", node.getOriginalScope());
            attribute("preManagedScope", node.getPremanagedScope());
            attribute("failedUpdateScope", node.getFailedUpdateScope());
            attribute("preManagedVersion", node.getPremanagedVersion());
            String state = null;
            switch (node.getState()) {
                case DependencyNode.INCLUDED:
                    state = "included";
                    break;
                case DependencyNode.OMITTED_FOR_CONFLICT:
                    state = "omittedForConflict";
                    break;
                case DependencyNode.OMITTED_FOR_CYCLE:
                    state = "omittedForCycle";
                    break;
                case DependencyNode.OMITTED_FOR_DUPLICATE:
                    state = "omittedForDuplicate";
                    break;
            }
            attribute("state", state);

            // TODO: where's the decription stored?

            return true;
        }


        private void attribute(String name, String value) {
            QuokkaTreeMojo.this.attribute(current, name, value);
        }

        public boolean endVisit(DependencyNode node) {
            current = (Element) current.getParentNode();
            return true;
        }

    }

    private void artifactAttributes(Artifact artifact, Element el) {
        attribute(el, "groupId", artifact.getGroupId());
        attribute(el, "artifactId", artifact.getArtifactId());
        attribute(el, "type", artifact.getType());
        attribute(el, "type", artifact.getType());
        attribute(el, "classifier", artifact.getClassifier());
        attribute(el, "scope", artifact.getScope());
        attribute(el, "version", artifact.getVersion());
        attribute(el, "optional", Boolean.toString(artifact.isOptional()));
        attribute(el, "snapshot", Boolean.toString(artifact.isSnapshot()));
        attribute(el, "release", Boolean.toString(artifact.isRelease()));
        attribute(el, "file", artifact.getFile() == null ? null : artifact.getFile().toString());
        attribute(el, "downloadUrl", artifact.getDownloadUrl());
    }

    private void attribute(Element el, String name, String value) {
        if (value != null && !value.equals("")) {
            el.setAttribute(name, value);
        }
    }

    /**
     * Copied from Artifact.VersionRange. This is tweaked to handle singular ranges properly. Currently the default
     * containsVersion method assumes a singular version means allow everything. This method assumes that "2.0.4" ==
     * "[2.0.4,)"
     *
     * @param allowedRange range of allowed versions.
     * @param theVersion   the version to be checked.
     * @return true if the version is contained by the range.
     */
    public static boolean containsVersion(VersionRange allowedRange, ArtifactVersion theVersion) {
        boolean matched = false;
        ArtifactVersion recommendedVersion = allowedRange.getRecommendedVersion();
        if (recommendedVersion == null) {

            for (Iterator i = allowedRange.getRestrictions().iterator(); i.hasNext() && !matched;) {
                Restriction restriction = (Restriction) i.next();
                if (restriction.containsVersion(theVersion)) {
                    matched = true;
                }
            }
        } else {
            // only singular versions ever have a recommendedVersion
            int compareTo = recommendedVersion.compareTo(theVersion);
            matched = (compareTo <= 0);
        }
        return matched;
    }
}